In diesem Kapitel beschäftigen wir uns damit, wie Klassen und Objekte miteinander zusammenhängen können und wie wir Klassen am besten zuschneiden und anordnen. Außerdem lernen wir die zwei Klassen Scanner
und PrintWriter
kennen, mit denen man in Java Dateien liest und schreibt (Input und Output = I/O).
Nebenbei lernen wir, präzisere UML-Diagramme zu zeichnen. UML steht für Unified Modeling Language und ist ein Standard, um Klassendiagramme (unter anderem) zu zeichnen, egal in welcher Programmiersprache die Klassen später implementiert werden sollen. Mit Hilfe von UML-Diagrammen können sich Programmierer austauschen und größere Projekte gemeinsam planen.
Schließlich lernen wir mit der Klasse HashSet
das Konzept einer Menge kennen - eine neue Form, mehrere Objekte zu speichern, bei der keine Duplikate auftreten können.
21.1 Social Network
Unser erstes Beispiel ist eine Art Sandkasten-Version von Facebook bzw. Google+.
Ziel unserer Anwendung ist es, eine Anzahl von Personen zu verwalten. Die Personen dürfen Nachrichten und Fotos posten und dürfen sich gegenseitig anfreunden.
Paket und Hauptklassen
Zunächst mal legen wir ein neues Paket augsbook
an,
d.h. alle Klassen innerhalb dieses Pakets müssen in der
ersten Zeile
package augsbook;
angeben. Auf der Festplatte legt Netbeans im src Verzeichnis das Unterverzeichnis augsbook an und speichert dort alle Klassen, dieses Pakets in Dateien mit der Endung .java.
Die Klasse Person
ist einfach. Nutzen Sie die NetBeans-Funktion Source > Insert Code,
um den Code schnell herzustellen.
Achten Sie auf die Zugriffsmodifikatoren
und die Getter-Methoden.
package augsbook; /** * Speichert Daten eines Augsbook-Mitglieds. */ public class Person { private String vorname; private String nachname; public Person(String vorname, String nachname) { this.vorname = vorname; this.nachname = nachname; } public String getVorname() { return vorname; } public String getNachname() { return nachname; } }
In einem UML-Klassendiagramm würden wir das so darstellen:
Ungewohnt ist, dass der Datentyp hinter den Variablennamen geschrieben wird nach dem Schema:
VARIABLENNAME : TYP
Bei Methoden wird der Rückgabetyp ebenfalls hinter die Methode geschrieben.
Die Zeichen vor den Variablen und Methoden sind Zugriffsmodifikatoren:
- Minus (-) bedeutet private
- Plus (+) bedeutet public
Das Netzwerk, also die Klasse SocialNetwork
, soll es zulassen, dass sich neue
Mitglieder registrieren. Das könnte im späteren Hauptcode so aussehen:
SocialNetwork augsbook = new SocialNetwork(); Person p1 = new Person("Barack", "Obama"); Person p2 = new Person("Angela", "Merkel"); augsbook.addMember(p1); augsbook.addMember(p2);
Ferner sollen wir neue Posts hinzufügen können:
MessagePost mp1 = new MessagePost("Yes, we can!", p1); MessagePost mp2 = new MessagePost("Alles, was noch nicht gewesen ist, ist Zukunft, wenn es nicht gerade jetzt ist.", p2); augsbook.addPost(mp1); augsbook.addPost(mp2);
Wir legen also eine Klasse SocialNetwork an, die sowohl eine Liste von Personen, die Mitglieder, verwaltet sowie eine Liste von Posts.
Im obigen Diagramm sehen Sie, dass in UML auch die Parameter einer Methode nach dem Schema "Variablenname : Typ" gelistet werden.
Achten Sie auf das
notwendige Importieren der Klasse
ArrayList
aus dem Paket
java.util
:
package augsbook; import java.util.ArrayList; /** * Ein soziales Netzwerk, das Mitglieder und Posts * verwaltet. */ public class SocialNetwork { private ArrayList<Post> posts = new ArrayList<Post>(); private ArrayList<Person> members = new ArrayList<Person>(); public void addMember(Person person) { members.add(person); } }
Ein Objekt vom Typ SocialNetwork
beinhaltet also mehrere Person-Objekte. Diese Beziehung kann man in einem UML-Klassendiagramm durch eine spezielle Verbindungsform darstellen:
Diese Beziehung nennt man in UML Aggregation.
Wie gehen wir mit Posts um? Zunächst muss es möglich sein, Posts hinzuzufügen, und auch, sie auszudrucken:
public class SocialNetwork { private ArrayList<Post> posts = new ArrayList<Post>(); private ArrayList<Person> members = new ArrayList<Person>(); public void addMember(Person person) { members.add(person); } public void addPost(Post post) { posts.add(post); } public void printPosts() { for (Post p: posts) { System.out.println(p.getDisplayText()); } } }
Sie sehen eine Klasse
Post
, die eine Methode
getDisplayText
haben muss. Dazu kommen wir im nächsten Abschnitt.
Posts: Lösung mit Klassenhierarchie
Bei den Posts haben wir zwei Sorten: einen Post für Textnachrichten, einen Post für
Bilder. Wir erstellen drei Klassen, eine Basisklasse Post
, die abstrakt ist, und zwei konkrete Klassen MessagePost
und PhotoPost
für die tatsächlichen Posts.
Achten Sie auf den Zugriffsmodifikator protected. Dieser lässt zu, dass die Unterklassen auf die Variablen zugreifen. In UML wird protected mit dem Hash-Symbol (#) gekennzeichnet.
package augsbook; import java.util.ArrayList; public abstract class Post { protected Person absender; protected int likes = 0; protected ArrayList<String> comments = new ArrayList<String>(); public Post(Person absender) { this.absender = absender; } public void addLike() { likes++; } public void addComment(String comment) { comments.add(comment); } public abstract String getDisplayText(); }
Hier die Unterklassen
MessagePost
und
PhotoPost
,
die jeweils eine Eigenschaft hinzufügen und die Methode getDisplayText
implementieren.
package augsbook; public class MessagePost extends Post { private String nachricht; public MessagePost(String nachricht, Person absender) { super(absender); this.nachricht = nachricht; } public String getDisplayText() { String result = "\"" + nachricht + "\" von " + absender.getNachname() + ", " + likes + " Likes"; for (String c: comments) { result += "\n Kommentar: " + c; } return result; } }
package augsbook; public class PhotoPost extends Post { private String filename; public PhotoPost(String filename, Person absender) { super(absender); this.filename = filename; } public String getDisplayText() { String result = "\"" + filename + "\" von " + absender.getNachname() + ", " + likes + " Likes"; for (String c: comments) { result += "\n Kommentar: " + c; } return result; } }
Die Tatsache, dass SocialNetwork
eine Liste von Post
-Objekten enthält, bedeutet, dass auch hier wieder die Beziehung namens Aggregation besteht:
Man kann die Verbindungslinie in UML auch beschriften. Die Beschriftung posts zeigt an, welche Instanzvariable die Aggregationsbeziehung herstellt.
Einstieg mit main
Jetzt fügen wir eine statische main-Methode in eine
unserer Klassen, um die Funktionstüchtigkeit zu testen.
Typischerweise wählen wir für die main-Methode die wichtigste Klasse, in diesem
Fall also
SocialNetwork
:
public static void main(String[] args) { SocialNetwork augsbook = new SocialNetwork(); Person p1 = new Person("Barack", "Obama"); Person p2 = new Person("Angela", "Merkel"); augsbook.addMember(p1); augsbook.addMember(p2); MessagePost mp1 = new MessagePost("Yes, we can!", p1); MessagePost mp2 = new MessagePost("Alles, was noch nicht gewesen ist, ist Zukunft, wenn es nicht gerade jetzt ist.", p2); PhotoPost pp1 = new PhotoPost("obama.jpg", p1); augsbook.addPost(mp1); augsbook.addPost(mp2); augsbook.addPost(pp1); mp1.addLike(); mp1.addLike(); mp1.addLike(); mp2.addLike(); mp1.addComment("Shut up"); mp1.addComment("I agree"); mp2.addComment("Wie bitte?"); augsbook.printPosts(); }
Die Ausgabe sieht so aus:
"Yes, we can!" von Obama, 3 Likes Kommentar: Shut up Kommentar: I agree "Alles, was noch nicht gewesen ist, ist Zukunft, wenn es nicht gerade jetzt ist." von Merkel, 1 Likes Kommentar: Wie bitte? "obama.jpg" von Obama, 0 Likes
Unser Programm hat noch einige Einschränkungen:
- Mitglieder können keine Freunde haben
- Wir wissen nicht, von wem die Kommentare kommen
- Wir wissen nicht, von wem die Likes kommen
Mengen mit HashSet
Zunächst mal wollen wir Freunde hinzufügen. Die Freunde speichern wir am besten in der Klasse Person. Speichern wir diese Freunde in einer Liste? Das Problem einer Liste ist, dass wir den selben Freund auch zweimal speichern könnten.
Um das zu verhindern, verwenden wir statt einer Liste
die Klasse
HashSet
. Diese Klasse versteht sich
als mathematische Menge (engl. set) und dort kann jedes Element
immer nur genau einmal vorkommen. Versucht man, das selbe Element
ein zweites Mal hinzuzufügen, wird das Hinzufügen ignoriert.
Eine weitere wichtige Eigenschaft von HashSet ist, dass die originale Reihenfolge nicht erhalten bleibt. Die Klasse behält sich also vor, die Reihenfolge (in der die Elemente ursprünglich hinzugefügt wurden) zu ändern, z.B. aus Effizienzgründen. Es zeigt sich, dass Reihenfolge nicht immer wesentlich ist, wie eben auch bei der Freundesliste.
Auch bei HashSet muss die Klasse importiert werden. Sie können die Elemente von HashSet genauso wie bei einer ArrayList mit der erweiterten For-Schleife durchlaufen:
import java.util.HashSet; public class SetDemo { public static void main(String[] args) { HashSet<String> menge = new HashSet<String>(); menge.add("Tiger"); menge.add("Hase"); for (String s: menge) { System.out.println(s); } } }
Jetzt ergänzen wir die Klassen Person:
package augsbook; import java.util.HashSet; public class Person { private String vorname; private String nachname; private HashSet<Person> friends = new HashSet<Person>(); ... public HashSet<Person> getFriends() { return friends; } public void addFriend(Person p) { friends.add(p); p.getFriends().add(this); } }
In UML verwenden wir wieder die Aggregation. Diesmal enthält ein Person-Objekt mehrere Objekte vom gleichen Typ Person:
Mehr Klassen
Wir wollen bei den Kommentaren wissen, wer den Kommentar abgegeben hat. Dazu erstellen wir eine neue Klasse, die sich den Kommentar (String) und den Autoren (Person) merkt:
package augsbook; public class Comment { private Person absender; private String text; public Comment(Person absender, String text) { this.absender = absender; this.text = text; } public Person getAbsender() { return absender; } public String getText() { return text; } }
In UML können bei unserer neuen Klasse Comment
anzeigen, dass Objekte dieses Typs immer auf ein Person-Objekt verweisen. Dies stellt nämlich eine Abhängigkeit zwischen den zwei Klassen her.
Man nennt diesen Pfeil eine navigierbare Assoziation. Man sieht, dass Objekte der einen Klasse auf ein Objekt der anderen Klasse zeigen. Dies ist wichtig beim Design von Klassenstrukturen, wo man darauf achten sollte, möglichst wenige Abhängigkeiten zwischen den Klassen zu haben.
Jetzt können wir unseren Post erweitern. Die Kommentare sind jetzt keine Liste von Strings, sondern eine Liste von Comment-Objekten. Die Likes werden jetzt nicht einfach gezählt, sondern die jeweilige Person wird sich gemerkt, und zwar in einer Menge (man darf nicht 2x liken).
package augsbook; import java.util.ArrayList; import java.util.HashSet; public abstract class Post { protected Person absender; protected HashSet<Person> likes = new HashSet<Person>(); protected ArrayList<Comment> comments = new ArrayList<Comment>(); ... public void addLike(Person p) { likes.add(p); } public void addComment(Person absender, String text) { comments.add(new Comment(absender, text)); } ... }
Natürlich müssen wir die Darstellung in MessagePost und PhotoPost ändern:
public class MessagePost extends Post { ... public String getDisplayText() { String result = "\"" + nachricht + "\" von " + absender.getNachname() + ", " + likes.size() + " Likes"; for (Comment c: comments) { result += "\n Kommentar: " + c.getText() + " (" + c.getAbsender().getNachname() + ")"; } return result; } }
Gefilterte Ausgabe von Posts
In SocialNetwork führen wir noch einen "Filter" ein, der es erlaubt, nur die Seite einer Person zu sehen, also z.B. die AugsBook-Seite von Angela Merkel. Diese bekommt natürlich nur Posts zu Gesicht, die sie selbst oder ein Freund geschrieben hat.
Beachten Sie die "private" Hilfsfunktion printFilteredPosts. Die Methode contains() prüft für eine Liste oder eine Menge, ob ein bestimmtes Objekt in dieser Liste enthalten ist.
public class SocialNetwork { private ArrayList<Post> posts = new ArrayList<Post>(); private ArrayList<Person> members = new ArrayList<Person>(); public void addPost(Post post) { posts.add(post); } public void addMember(Person person) { members.add(person); } private void printFilteredPosts(Person person) { for (Post p : posts) { if (p.getAbsender().equals(person) || person.getFriends().contains(p.getAbsender())) { System.out.println(p.getDisplayText()); } } } public void showPersonPage(Person person) { System.out.println("Page of " + person.getFullName()); System.out.println("============================"); System.out.println("Friends:"); for (Person p : person.getFriends()) { System.out.println(" - " + p.getFullName()); } printFilteredPosts(person); } public static void main(String[] args) { SocialNetwork augsbook = new SocialNetwork(); Person p1 = new Person("Barack", "Obama"); Person p2 = new Person("Angela", "Merkel"); Person p3 = new Person("Jay", "Z"); Person p4 = new Person("Maggie", "Thatcher"); augsbook.addMember(p1); augsbook.addMember(p2); p1.addFriend(p2); p1.addFriend(p3); MessagePost mp1 = new MessagePost("Yes, we can!", p1); MessagePost mp2 = new MessagePost("Alles, was noch nicht gewesen ist, ist Zukunft, wenn es nicht gerade jetzt ist.", p2); PhotoPost pp1 = new PhotoPost("obama.jpg", p1); MessagePost mp3 = new MessagePost("Yo", p3); augsbook.addPost(mp1); augsbook.addPost(mp2); augsbook.addPost(pp1); mp1.addLike(p2); mp1.addLike(p3); mp2.addLike(p1); mp1.addComment(p4, "Shut up"); mp1.addComment(p2, "I agree"); mp2.addComment(p1, "Wie bitte?"); augsbook.showPersonPage(p1); System.out.println("\n"); augsbook.showPersonPage(p2); } }
Unsere Ausgabe ist jetzt:
Page of Barack Obama ================================== Friends: - Jay Z - Angela Merkel "Yes, we can!" von Obama, 2 Likes Kommentar: Shut up (Thatcher) Kommentar: I agree (Merkel) "Alles, was noch nicht gewesen ist, ist Zukunft, wenn es nicht gerade jetzt ist." von Merkel, 1 Likes Kommentar: Wie bitte? (Obama) "obama.jpg" von Obama, 0 Likes Page of Angela Merkel ================================== Friends: - Barack Obama "Yes, we can!" von Obama, 2 Likes Kommentar: Shut up (Thatcher) Kommentar: I agree (Merkel) "Alles, was noch nicht gewesen ist, ist Zukunft, wenn es nicht gerade jetzt ist." von Merkel, 1 Likes Kommentar: Wie bitte? (Obama) "obama.jpg" von Obama, 0 Likes
Sehen wir uns zum Schluss noch das komplette Klassendiagramm an. Dieses zeigt auf einem Blick und unabhängig von der konkreten Programmiersprache (z.B. Java, C# oder Swift) die Inhalte der Klassen und ihre Beziehungen untereinander an.
Übungsaufgaben
(a) ArrayList vs HashSet
Schreiben Sie ein kleines Programm, mit dem Sie eine Liste und eine Menge vergleichen. Es reicht eine Klasse mit einer statischen main-Methode, die ausgeführt wird.
Konkret erstellen Sie eine ArrayList von Strings (namensListe) und einen HashSet von Strings (namensMenge).
Fügen Sie bei beiden die Namen "Harry" und "Sally" jeweils zwei Mal hinzu. Das heißt, Sie rufen sowohl für die Liste als auch für die Menge 4x die Methode add() auf.
Durchlaufen Sie anschließend sowohl Liste als auch Menge mit einer erweiterten For-Schleife und drucken Sie die Elemente aus.
Zusammenfassung
In UML werden Klassen als Boxen mit drei Abschnitten gezeichnet für (1) Klassenname, (2) Instanzvariablen und (3) Methoden.
In UML werden Zugriffsmodifikatoren mit + (public), - (private) und # (protected) ausgedrückt. Der Datentyp wird hinter die Variable/Methode geschrieben nach dem Schema VARIABLE : TYP.
In UML werden Beziehungen zwischen Klassen ausgedrückt. Enthält eine Klasse A mehrere Objekte einer Klasse B, so besteht eine Aggregation. Diese wird mit einem leeren Diamanten (bei A) und einer Linie zu B gezeichnet. Viel simpler ist die Beziehung Assoziation. Eine Assoziation zwischen den Klassen C und D besteht dann, wenn C mindestens ein Objekt vom Typ D enthält. Dann zeichnet man einen einfachen Pfeil von C nach D.
Die Klasse HashSet
funktioniert fast genauso wie ArrayList
. Der Unterschied: jedes Objekt kann höchstens einmal in einem HashSet vorkommen. Ruft man also add()
auf einem HashSet auf, so wird zunächst geprüft, ob das übergebene Objekt bereits enthalten ist. Wenn ja, wird es nicht noch einmal gespeichert.
21.2 I/O in Java
I/O steht in der Informatik für Input/Output und meint das Lesen von Dateien, Internet, Tastatur (Input) oder das Schreiben auf Dateien, Internet, Konsole, Grafikbereich (Output). In diesem Abschnitt konzentrieren wir uns auf Dateien I/O und Tastaturinput.
Auch in Java müssen wir immer wieder Daten auf die Festplatte schreiben. Leider kennt Java nicht die Processing-Funktionen loadStrings() und saveStrings(), die wir in Kap. 17 kennen gelernt haben.
Stattdessen stellt Java die zwei Klassen Scanner
zum Lesen und PrintWriter
zum Schreiben bereit.
Dateien und Pfade
Klasse File
Bevor wir zum Lesen/Schreiben kommen, sollten wir uns die Klasse File
aus dem Paket java.io
anschauen. Bislang haben wir unsere Dateinnamen und Pfade mit einem einfachen String repräsentiert. Das bringt so banale Probleme mit sich wie die Frage nach dem Schrägstrich (vorwärts wie unter Linux/Mac oder rückwärts wie unter Windows?). Stattdessen sollen Pfade als File-Objekte repräsentiert werden.
Im einfachsten Fall erzeugen wir einfach ein File-Objekt, indem wir den Dateipfad als String übergeben:
File file1 = new File("C:/Foo/Baa/meineEinkaufsliste.txt"); // absolut File file2 = new File("freunde.txt"); // relativ
Bei file1
sehen Sie einen absoluten Pfad, weil
er von der "Wurzel" des Dateisystems (hier wäre das C:) bis zum Ziel alles
auflistet. Ein relativer Pfad wie bei file2
geht von der aktuellen Position des Systems aus, das ist bei NetBeans das
aktuelle Projektverzeichnis. Wenn Ihr Netbeans-Projekt "Foo"
heißt, dann ist das Projektverzeichnis eben das Verzeichnis "Foo" (darunter
befinden sich dann src und weitere Verzeichnisse).
Weitere Beispiele für relative Pfade sind:
File file3 = new File("trash/feinde.txt"); // in einem Unterverz. File file4 = new File("../chefs.txt"); // im übergeordneten Verz.
Pfade zusammensetzen
Häufig liegen Dateien nicht genau im Projektverzeichnis, sondern z.B. im Home-Verzeichnis des Benutzers oder ganz woanders. Sie können Pfade als String zusammensetzen:
String pfad = "C:/users/kipp"; String dateiname = "foo.txt"; File file = new File(pfad + "/" + dateiname); System.out.println("f: " + file);
Dabei sollten Sie darauf achten, immer den Forward-Slash (/) zu verwenden, und nicht den Backward-Slash(\). Der Forward-Slash funktioniert unter allen Betriebssystemen (Win, Mac, Linux), der Backward-Slash nur unter Windows.
Man sollte allgemein versuchen, den Slash zu vermeiden. Die Klasse
File
bietet einen Konstruktor an, wo Sie Pfad und Dateiname
als Parameter übergeben und des komplette Pfade dann von Java gebaut wird.
Der obige Code sieht dann so aus:
String pfad = "C:/users/kipp"; String dateiname = "foo.txt"; File file = new File(pfad, dateiname); System.out.println("f: " + file);
Java kann Ihnen außerdem zwei Verzeichnisse als absolute Pfade liefern
mit der Methode System.getProperty()
.
Mit
System.getProperty("user.dir")
bekommen Sie das aktuelle
Projektverzeichnis (also da, wo Ihr Code liegt). Mit
System.getProperty("user.home")
bekommen Sie das
Home-Verzeichnis des aktuellen Users (also auf dem Mac z.B. "C:/users/schmidt").
String user = System.getProperty("user.dir"); String home = System.getProperty("user.home"); System.out.println(user); System.out.println(home);
Sie können dies benutzen, um Ihren gewünschten Pfad zusammenzubauen.
Methoden von File
Einen Pfad in ein File-Objekt "einzupacken" ist für viele Klassen und Methoden notwendig, damit diese den Pfad nutzen können. Die Klasse File
ist aber auch für sich genommen nützlich. So können Sie das Objekt befragen, z.B. ob die Datei existiert, ob es sich um Datei oder Verzeichnis handelt etc.
File file = new File("C:\Foo\Baa\meineEinkaufsliste.txt"); // Jetzt die Befragung: if (file.exists()) { System.out.println("Ich bin!"); } if (file.isFile()) { System.out.println("Ich bin eine Datei!"); } if (file.isDirectory()) { System.out.println("Ich bin ein Verzeichnis!"); }
Mit toString()
können Sie auch jederzeit wieder den Pfad ansehen oder ausgeben. Bei System.out.print wird toString() wie gewohnt automatisch aufgerufen.
File file = new File("C:\Foo\Baa\meineEinkaufsliste.txt"); System.out.println("... und der Pfad ist ... " + file);
Ansonsten können Sie noch andere tolle Dinge mit der Klasse File
anstellen, z.B. Dateien löschen, den Inhalt von Verzeichnissen untersuchen, Verzeichnisse erzeugen uvm.
Schauen Sie in die API von File, um mehr zu erfahren.
Datei lesen mit Scanner
Die Klasse Scanner
aus dem Paket java.util
wurde speziell zum Einlesen von verschiedenen Datentypen (Zahlen, Texten etc.) gemacht. Wir beschäftigen uns hier mit Text in Form von Strings.
Ob ein Scanner seine Daten aus einer Datei oder aus dem Internet oder von der Tastatur des Users liest, ist übrigens egal. Der Scanner ist sehr allgemein gehalten. Zunächst mal gehen wir aber davon aus, dass wir eine Datei einlesen wollten.
Zunächst erzeugt man ein Scanner-Objekt, indem man ihm ein File-Objekt zum öffnen übergibt.
File file = new File("secretStuff.txt"); Scanner scanner = new Scanner(file);
Das Scanner-Objekt funktioniert wie ein Cursor, den wir durch die Datei bewegen. Der Cursor springt zum Beispiel zeilenweise durch den Text und gibt dabei die übersprungene Zeile zurück.
Das Scanner-Objekt hat dafür die Methoden hasNextLine()
(ist true, wenn es noch eine Zeile zum Überspringen gibt) und nextLine()
(überspringt die nächste Zeile).
Die Methoden lassen sich wie folgt in eine While-Schleife einbauen:
while (scanner.hasNextLine()) { String line = scanner.nextLine(); System.out.println("> " + line); }
Nach Verwendung des Scanners, sollte der "Datenstrom" geschlossen werden (engl. close), damit gebundene Ressourcen freigegeben werden:
scanner.close();
Der Code wird in dieser Form nicht laufen, da der Compiler etwas über eine "Exception" erzählt und sich weigert, den Code zu compilieren. Schuld ist die Zeile new Scanner(file)
. Hier könnte es sein, dass das übergebene File nicht existiert - in Java-Speak sagt man, dass eine FileNotFoundException geworfen werden könnte. Wir behandeln die Behandlung von Exceptions erst später, deshalb müssen Sie sich mit der Info begnügen, dass Sie folgendes try-catch-Konstrukt um den Code herumbauen müssen:
try { File file = new File("secretStuff.txt"); Scanner scanner = new Scanner(file); while (scanner.hasNextLine()) { String line = scanner.nextLine(); System.out.println("> " + line); } scanner.close(); } catch (FileNotFoundException ex) { ex.printStackTrace(); }
Bevor wir uns mit dem Schreiben von Daten beschäftigen, sehen wir uns eine andere nützliche Verwendung von Scanner an.
Tastaturinput mit Scanner
Ein Scanner-Objekt kann auch benutzt werden, um Tastatureingaben von Benutzern einzulesen. Anstatt eine Datei als Datenstrom anzugeben, verwendet man den Kanal System.in
(wird auch häufig standard input genannt). Im Code sieht das so aus:
Scanner scanner = new Scanner(System.in);
Wenn man jetzt nach der nextLine()
fragt, wartet Java so lange, bis der Benutzer die Enter-Taste drückt und der davor eingegebene Text wird zurückgegeben. Denken Sie immer an das Schließen des Scanners mit close()
.
Scanner scanner = new Scanner(System.in); System.out.print("Write something: "); String input = scanner.nextLine(); System.out.println("You wrote: " + input); scanner.close();
In dem obigen Beispiel fangen Sie genau eine Eingabe ab. Häufig möchten Sie aber viele Eingaben verarbeiten - bis zum Beispiel der Benutzer "exit" eintippt. Hier bietet sich eine While-Schleife an, die die Methode hasNextLine()
von Scanner benutzt, um herauszufinden, ob eine neue Zeile zur Verfügung steht.
Scanner scanner = new Scanner(System.in); boolean exit = false; // Zum Beenden System.out.print("Say something: "); while (!exit && scanner.hasNextLine()) { String line = scanner.nextLine(); if (line.toLowerCase().startsWith("exit")) { exit = true; } else { System.out.println("Did you say this: " + line); System.out.print("Say something: "); } } System.out.println("Bye bye"); scanner.close();
Sie sehen, dass wir hier noch auf das Wort "exit" testen und dann aus der While-Schleife herausspringen, sollte es auftauchen.
Zu beachten ist hier, dass hasNextLine()
bereits wartet (bis ein Enter erfolgt). Deshalb darf man z.B. die Reihenfolge in der While-Bedingung nicht umdrehen.
Datei schreiben mit PrintWriter
Zum Schreiben stellt Java die Klasse PrintWriter
zur Verfügung. Sie erstellen ein Objekt dieses Typs und übergeben ihm den Dateipfad der Zieldatei.
File file = new File("meinzeug.txt"); PrintWriter writer = new PrintWriter(file);
Anschließend können wir die Methode println()
verwenden, wie wir sie in Processing und bei System.out verwendet haben.
writer.println("Agent 001"); writer.println("Agent 003"); writer.println("Agent 005");
Hier ist besonders wichtig, den Datenstrom zu schließen, weil es sonst passieren kann, dass die Datei nicht korrekt gespeichert wird. Dann sind manchmal alle Daten verloren.
writer.close();
Genauso wie bei Scanner muss auch hier eine Exception gefangen werden, wenn das PrintWriter-Objekt hergestellt wird.
try { File file = new File("C:/Users/Bond/agenten.txt"); PrintWriter writer = new PrintWriter(file); writer.println("Agent 001"); writer.println("Agent 003"); writer.println("Agent 005"); writer.close(); } catch (FileNotFoundException ex) { ex.printStackTrace(); }
Übungsaufgaben
(a) Text lesen
Erstellen Sie ein Projekt mit einer einzigen Klasse
TextReader
Legen Sie im Projektverzeichnis eine Textdatei an
mit ein paar Zeilen Text, z.B.
Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.
Schreiben Sie die Methode readFile
, die
einen Dateipfad als String bekommt und den (Text-)Inhalt
dieser Datei einliest und als Liste von Strings
zurückgibt.
Testen Sie die Methode mit Ihrer Textdatei in der statischen main. Geben Sie den Inhalt der Liste zum Testen aus.
(b) Protokollprogramm (Text schreiben)
Schreiben Sie ein Programm, dass Text von der Konsole einliest und direkt in eine Datei schreibt.
Auf der Konsole erscheint ein "Prompt":
input>
Der Benutzer kann Text schreiben und mit ENTER die Zeilen bestätigen:
input> Hallo, Welt input> Mein Name ist Hase
Mit einem Schlüsselwort kann die Eingabe beendet werden und das Programm zeigt an, wohin der Text gespeichert wurde. Das Schlüsselwort selbst soll nicht mit gespeichert werden.
input> Hallo, Welt input> Mein Name ist Hase input> QUIT Wrote text to: protokoll.txt
Schreiben Sie eine Klasse NoteWriter mit einer Methode start(). Die Methode bekommt den Dateinamen des gewünschten Textfiles als String übergeben. Verwenden Sie die oben besprochnen Klassen Scanner und PrintWriter.
Testen Sie Ihr Programm mit z.B. folgendem Code in der main:
public static void main(String[] args) { NoteWriter noteWriter = new NoteWriter(); noteWriter.start("protokoll.txt"); }
Sehen Sie sich beim Testen immer den Text in der Textdatei mit einem Editor an.
(c) Einfacher Chatbot
Schreiben Sie einen einfachen Chatbot, wo der Benutzer Text in der Konsole eingibt und nach dem Betätigen von ENTER eine (Text-)Antwort erhält.
Erstellen Sie dazu eine Klasse Chatbot
mit
der Methode start(). In der Methode start() lesen Sie mit einer
while-Schleife den Input von der Konsole (mit Scanner). Kopieren Sie ruhig
den Code aus dem obigen Abschnitt.
Aufgabe ist, auf die folgenden Inputs wie folgt zu reagieren:
- Input "hallo": Output "Guten Tag, ich bin ein Chatbot."
- Input "wie gehts": Output "Super!"
- Input "tschüs": Programm endet
- Sonstiger Input: Output "Das habe ich leider nicht verstanden."
Gutes Design wäre, die Verarbeitung des Inputs und die Generierung des Outputs in eine eigene Methode (z.B. parseInput) zu packen. Die Methode bekommt eine String (Eingabe), liefert einen String zurück (Ausgabe) und wird in start() verwendet.
(d) Intelligenter Chatbot
Versuchen Sie, Ihren Chatbot intelligenter zu machen. Dazu müssen Sie versuchen, auf bestimmte Schlüsselwörter (oder auch Interpunktion) in der Benutzereingabe zu reagieren. Schauen Sie sich dazu nochmal die Möglichkeiten an, einen String zu untersuchen, entweder in Kap. 12 (Dateien und Text) oder direkt in der Java API für die Klasse String.
Einige Vorschläge:
- Unterscheiden Sie zwischen Fragen und Aussagen und reagieren Sie, wenn Sie jeweils die Eingabe nicht verstehen, mit "Das habe ich nicht verstanden." oder "Die Frage habe ich nicht verstanden."
- Reagieren Sie auf den Textschnipsel "was ist" und antworten Sie, indem Sie das Folgewort wieder in den Output schreiben. Beispiel: "was ist Informatik" => Antwort "Informatik ist ein gutes Thema."
- Fragen Sie den Benutzer nach dem Namen und versuchen Sie, den Namen aus der Antwort zu extrahieren und ihn in einer Instanzvariablen abzulegen, so dass Sie ihn immer wieder mal (30% Wahrscheinlichkeit) an die Antwort hängen können ("Informatik ist ein gutes Thema, Martin.").
Sie können hier beliebig tief einsteigen in die Themen Computerlinguistik und Künstliche Intelligenz. Wenn Sie sich für die Geschichte der Chatbots interessieren, schauen Sie nach den Stichworten "Eliza", "Turing Test" und "Loebner Award".
Zusammenfassung
Die Klasse Scanner
erlaubt das Lesen von Dateien und Tastatureingaben.
Um Dateien zu lesen, erzeugen Sie ein Scanner-Objekt,
indem Sie die zu lesende Datei als File
übergeben.
Dann verwenden Sie hasNextLine()
und nextLine()
,
um mit einer While-Schleife zeilenweise durch die Datei zu gehen.
Sie müssen try-catch
benutzen, um einen Fehlerfall
abzufangen (FileNotFoundException). Im einfachsten Fall legen
Sie die zu lesende Datei im Projektverzeichnis ab
(nicht in /src und auch nicht im Paket).
Um Benutzerinput per Tastatur zu lesen, erzeugen Sie das Scanner-Objekt mit System.in
als Argument. Anschließend können Sie wieder mit hasNextLine()
und nextLine()
die Eingaben des Benutzers lesen.
In allen Fällen müssen Sie den Scanner mit close()
schließen.
Zum Schreiben von Daten gibt es die Klasse PrintWriter
. Erzeugen Sie das Objekt mit dem File
, in das geschrieben werden soll und verwenden Sie anschließend println()
, um die Daten ins File zu schreiben. Auch hier muss ein try-catch
verwendet werden. Auch hier muss der PrintWriter mit close()
geschlossen werden.
21.3 Projekt: Lernen mit Karteikarten
Dieses Projekt ist eine durchgehende Übung, bei der die Lösung nur skizziert wird. Füllen Sie selbst die Lücken bei der Implementierung des Projekts.
Eine beliebte und effiziente Methode, um sich Wissen anzueignen, ist das Lernen mit Karteikarten nach dem System von Sebastian Leitner.
Lernen mit Karteikarten
Hier sei das System kurz erklärt (mehr unter obigem Link). Sie benötigen dazu Karteikarten und einen Karteikasten mit drei Fächern, die wir 1, 2 und 3 nennen (das System funktioniert auch mit mehr Fächern, häufig werden 5 benutzt).
Nehmen wir an, Sie wollen Englisch-Vokabeln lernen. Sie nehmen einen Stapel Karteikarten und schreiben auf die Vorderseite die englische Vokabel (Frage), auf die Rückseite die deutsche Vokabel (Antwort). Anschließend deponieren Sie den Stapel in Fach 1.
Jeden Tag gehen Sie durch alle Karten in Fach 1. Sie schauen auf die Vorderseite und versuchen, sich an die deutsche Übersetzung zu erinnern. Dann drehen Sie die Karte um. Wenn Sie sich richtig erinnert haben, kommt die Karte in das nächsthöhere Fach (Fach 2). Wenn nicht, bleibt es in Fach 1. Jeden dritten Tag gehen Sie durch Fach 2. Die Karten, die Sie wussten, kommen in Fach 3. Die Karten, die Sie nicht mehr wussten, kommen zurück in Fach 1! Jede Woche gehen Sie durch Fach 3 in ähnlicher Weise (nur dass die Karten nicht mehr in ein höheres Fach wandern können).
Wenn Sie neue Vokabeln hinzufügen, kommen diese immer in Fach 1. Fach 1 sollte nicht zu voll werden, bei etwa 20 bis 30 Karten sollte man es belassen.
Warum ist das System effizient? Weil unsicheres Wissen öfter wiederholt wird als das, was man schon sicher weiß. Warum ist es motivierend? Weil man an der Anzahl der Karten sieht, wieviel man schon gelernt hat, und weil man an der Position (Fach) direkt sieht, wieviel Wissen schon sehr gut "sitzt". Kann man mit dem System nur Vokabeln lernen? Nein! Auch Fragen zu Java kann man auf Karteikarten formulieren. Zum Beispiel: "Wie sieht eine Foreach-Schleife aus?" oder "Was bedeutet der Zugriffmodifikator protected?". Sie können sogar kleine Übungsaufgaben auf der Vorderseite formulieren, die Sie so oft wiederholen, bis Sie sie im Schlaf können.
Klassendesign
Sie benötigen drei Klassen: Karteikarte
für die Karteikarten, Fach
für die einzelnen Fächer und eine Art "Hauptklasse", nenne wir sie Karteikasten
. Es könnte schließlich mehrere Lernprojekte und damit Karteikästen geben, für verschiedene Sprachen oder für verschiedene Personen.
Hier eine erste (unvollständige) Skizze als UML-Klassendiagramm.
Ein Objekt vom Typ Karteikasten enthält mehrere (bei uns zunächst mal drei) Fächer, jeweils vom Typ Fach. Ein Fach enthält mehrere Karteikarten.
Übung: Karteikarte und Fach
(a) Karteikarte
Schreiben Sie zunächst die Klasse Karteikarte
mit den zwei Eigenschaften frage
und antwort
. Sie benötigen ferner einen Konstruktor und Getter-Methoden für die Eigenschaften. Schreiben Sie außerdem eine toString() Methode.
Testen Sie die Klasse mit einer eigenen main-Methode, in der Sie ein Testobjekt erzeugen und auf der Konsole ausgeben.
Hinweis: Wenn Sie in NetBeans eine bestimmte main-Methode aufrufen wollen, dann stellen Sie den Cursor auf den Klassennamen (hinter "public class") und wählen mit Rechtsklick Run File.
(b) Fach
Als nächstes schreiben Sie die Klasse Fach
. Diese repräsentiert ein Karteikasten-Fach und beinhaltet mehrere Karteikarten. Welche Methoden wir genau benötigen, wird sich im Laufe des Projekts herausstellen. Zu Beginn genügt eine Methode addKarte(Karteikarte k)
, welche eine Karte der Liste hinzufügt.
Zum Testen der Klasse ergänzen Sie doch eine Methode
printKarten()
, die alle Karten im Fach auf
der Konsole ausgibt.
Klasse Karteikasten
Wir konzentrieren uns jetzt auf die Hauptklasse. Im Konstruktor müssen drei Fächer hergestellt werden und in die Liste faecher
gefügt werden. Damit wir Daten haben, kümmern wir uns um das Laden und Speichern von Karteikarten (siehe auch Kap. 23.2 oben).
Jetzt benötigen wir in Karteikasten
folgende Methoden, die Sie noch implementieren müssen:
- printFaecher
- load
- save
Die Methode printFaecher()
soll ausgeben, wieviele Karten jeweils in einem Fach sind, z.B. so:
Fach 0: 2 Fach 1: 1 Fach 2: 1
Wie sollen Karteikarten gespeichert werden? Wir wollen die Karten aller Fächer in eine Datei schreiben. Das Dateiformat soll wie folgt aussehen:
Fach F: Was heißt private? A: Zugriff nur von eigener Klasse aus möglich F: Warum Java? A: Populäre Sprache, plattformunabhängig, objektorientiert, Android Fach F: Wie prüft man den dynamischen Typ einer Variablen? A: x instanceof Foo Fach F: Was ist "this"? A: Zeiger auf eigenes Objekt, wird für Instanzvariablen im Konstruktor benutzt
Unser Speicherformat ist in Abschnitte geteilt. Jeder Abschnitt wird von "Fach" angeführt, damit die Einlese-Methode weiß, wann ein neues Fach beginnt. Dann werden die Karteikarten auf je zwei Zeilen für Frage (F) und Antwort (A) aufgelistet. Da wir nicht wissen, wieviele Karten gespeichert sind, müssen wir in load()
offenbar mit einer While-Schleife arbeiten.
Übung: Karteikasten 1
(c) Karteikasten
Beginnen Sie mit der Implementierung der Klasse Karteikasten
. Schreiben Sie zunächst einen Konstruktor, wo die drei Fächer erzeugt werden. Sie können jetzt zum Testen einige Beispielkarten im Konstruktor anlegen und auf die Fächer verteilen.
Dann schreiben Sie die Methode printFaecher()
. Wird die richtige Anzahl der (Test-)Karten in den jeweiligen Fächer ausgegeben?
(d) Laden und Speichern
Schließlich schreiben Sie save() und load(). Sie könnten zunächst die Testkarten speichern. Anschließend können Sie den Code für die Testkarten aus dem Konstruktor löschen.
Testen Sie die Methoden save()
und load()
in der main()-Methode von Karteikasten
, zum Beispiel wie folgt:
- Erzeugen Sie ein Datenfile mit einem Texteditor mit Namen "java.txt" (Sie können z.B. die Beispieldaten im obigen Abschnit nehmen)
- In Ihrer main-Methode erzeugen Sie ein Karteikasten-Objekt und lesen Sie die Daten aus "java.txt" mit load() ein
- Drucken Sie mit printFaecher() die Zahlen aus - stimmt alles?
- Fügen Sie per Code eine Frage in Fach 0 hinzu und speichern Sie die Fächer wieder mit save(), allerdings in "java-test.txt" - schauen Sie in die Datei; taucht die neue Frage auf?
Karten abfragen, die Klasse Random
Am wichtigsten ist eine neue Methode namens lerneFach(int fachNum)
. Diese fragt nur die Karten aus einem Fach ab. Dazu wird zunächst eine Karte zufällig ausgewählt und die Frage ausgegeben:
F: Was ist this? -> Press Enter to see the answer
Anschließend wird auf die Eingabe des Benutzers gewartet. Wenn dieser Enter drückt, erscheint:
A: Zeiger auf eigenes Objekt, wird für Instanzvariablen im Konstruktor benutzt -> Press Y+Enter if you knew this or Enter if not (Q to quit)
Wieder warten wir auf Benutzerinput. Bei "y" verschieben wir die Karte in das nächsthöhere Fach (es sei denn, wir sind bereits im dritten Fach) - diese Bewegung zeigen wir auch auf der Konsole an. Bei "q" beenden wir die Abfrage. Bei allen anderen Eingaben hat der Benutzer die Karte nicht gewusst, d.h. sie kommt in Fach 0 (es sei denn, wir sind gerade in Fach 0).
Hier ein kompletter Ablauf:
F: Was ist this? -> Press Enter to see the answer A: Zeiger auf eigenes Objekt, wird für Instanzvariablen im Konstruktor benutzt -> Press Y+Enter if you knew this or Enter if not (Q to quit) y [Moved card from 1 to 2] F: Warum Java? -> Press Enter to see the answer A: Populäre Sprache, plattformunabhängig, objektorientiert, Android -> Press Y+Enter if you knew this or Enter if not (Q to quit) [Moved card from 1 to 0]
Jetzt eine Vorüberlegung: Nehmen wir an, Sie lernen die Karten in Fach 0, es sind fünf Karten vorhanden. Ihr Programm wählt aus allen Karten in Fach 0 zufällig eine aus und befragt Sie. Sie bearbeiten die erste Karte und sehen sich die Antwort an. Nicht gewusst! Jetzt muss die Karte in Fach 0 bleiben. Jetzt soll die nächste Karte gelernt werden, aber diese soll natürlich aus den restlichen vier Karten ausgewählt werden. Sie wollen nicht, dass die Karte in dieser Sitzung ein zweites Mal abgefragt wird. Wie lösen wir das Problem? Denken Sie darüber nach. Ein Lösungsansatz ist im Übungsabschnitt formuliert.
Für die zufällige Wahl einer Karte in einem Fach benötigen Sie eine Zufallszahl zwischen 0 und N-1 (wobei N die Anzahl der im Fach enthaltenen Karten ist). Zufallszahlen funktionieren in Java anders als in Processing. In Java steht Ihnen eine Klasse Random
zur Verfügung. Sie erzeugen zunächst ein Random-Objekt mit einer beliebigen natürlichen Zahl. Diese Zahl soll sicherstellen, dass die erzeugten Zahlen wirklich zufällig sind und sollte jedesmal, wenn Sie ein Random-Objekt erzeugen, anders sein - diese Zahl wird seed (engl. für Samen) genannt. Wir nehmen einfach die aktuelle Uhrzeit Ihres Computers, ausgedrückt in Millisekunden, die seit dem 1.1.1970 vergangen sind (eine häufige Konvention), als seed und speichern unser Objekt als Instanzvariable in der Klasse Fach
:
private Random random = new Random(System.currentTimeMillis());
Wenn Sie jetzt eine Zufallszahl von 0 bis 9 benötigen, befragen Sie Ihr Random-Objekt nach der "nächsten" Zufallszahl:
int num = random.nextInt(10);
Die "10" ist bei Ihnen natürlich eine Variable oder ein sonstiger Ausdruck. Denken Sie daran, dass diese Zahl nicht im Raum möglicher Zahlen enthalten ist, d.h. hier bekommen wir eine Zahl aus der Menge {0, 1, ..., 9}.
Als Orientierung für Ihre Implementierung sehen Sie hier die vollständige UML-Darstellung der Klasse Fach
:
Übung: Karteikasten 2
Folgen Sie der Anleitung im obigen Abschnitt, um die Klasse Karteikasten
fertig zu implementieren und dabei auch die Klasse Fach
zu vervollständigen.
(e) Karten in einem Fach lernen
Wir haben gesehen, dass es ein Problem darstellt, eine Karte zufällig zu wählen und in der nächsten Runde aus den verbleibenden Karten zu wählen. Wie lösen wir dies?
Eine Möglichkeit ist, zu Beginn des Lernens im Fach alle Karten in eine Extra-Liste zu speichern. Diese Liste wird zum Lernen benutzt, nennen wir sie also lernkarten
. Wenn wir eine Karte daraus abgearbeitet haben, egal ob gewusst oder nicht, wird die Karte aus lernkarten entfernt.
Sie benötigen also in Fach folgende Methoden:
- resetLernkarten(): legt die Liste lernkarten neu an
- numLernkarten(): Anzahl der verbleibenden Lernkarten
- waehleLernkarte(): Wählt zufällig eine Karte aus lernkarten aus, entfernt diese aus lernkarten und gibt sie zurück
(f) Lernsitzungen
Wenn Sie lerneFach
implementiert haben, könnten Sie in Karteikasten
eine Methode lerne
implementieren, in der Sie durch alle Fächer durchgehen, um besten in der Reihenfolge 2-1-0 (warum?).
Sie können sich außerdem überlegen, wie Sie die Lernmethode von Leitner implementieren. Jeden Tag Fach 0 lernen, jeden dritten Tag Fach 1, jede Woche Fach 2.
Mit einem Datum zu arbeiten, muss vielleicht erstmal nicht sein. Nehmen wir stattdessen die Lernsitzung, eine Zahl, und zählen diese nach jedem Lernen hoch. Die angepasste Regel lautet: in jeder Sitzung Fach 0 lernen, in jeder dritten Sitzung Fach 1 (und 0), in jeder siebten Fach 2 (und 1 und 0).
Wo wird die Sitzung implementiert (Klasse)? Wie wird sie gespeichert?